/*
 * JFugue - API for Music Programming
 * Copyright (C) 2003-2008  David Koelle
 *
 * http://www.jfugue.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

package org.jfugue;

import java.io.File;
import java.io.IOException;
import java.net.URL;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiFileFormat;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Synthesizer;

/**
 * Prepares a pattern to be turned into music by the Renderer. This class also
 * handles saving the sequence derived from a pattern as a MIDI file.
 *
 * @see MidiRenderer
 * @see Pattern
 * @author David Koelle
 * @version 2.0
 */
public class Player {
	private Sequencer sequencer;
	private MusicStringParser parser;
	private MidiRenderer renderer;
	private float sequenceTiming = Sequence.PPQ;
	private int resolution = 120;
	private boolean paused = false;
	private boolean started = false;
	private boolean finished = false;

	/**
	 * Instantiates a new Player object, which is used for playing music.
	 */
	public Player() {
		this(true);
	}

	/**
	 * Instantiates a new Player object, which is used for playing music. The
	 * <code>connected</code> parameter is passed directly to
	 * MidiSystem.getSequencer. Pass false when you do not want to copy a live
	 * synthesizer - for example, if your Player is on a server, and you don't
	 * want to create new synthesizers every time the constructor is called.
	 */
	public Player(boolean connected) {
		try {
			// Get default sequencer.
			setSequencer(MidiSystem.getSequencer(connected)); // use non
																// connected
																// sequencer so
																// no copy of
																// live
																// synthesizer
																// will be
																// created.
		} catch (MidiUnavailableException e) {
			throw new JFugueException(
					JFugueException.SEQUENCER_DEVICE_NOT_SUPPORTED_WITH_EXCEPTION
							+ e.getMessage());
		}
		initParser();
	}

	/**
	 * Creates a new Player instance using a Sequencer that you have provided.
	 * 
	 * @param sequencer
	 *            The Sequencer to send the MIDI events
	 */
	public Player(Sequencer sequencer) {
		setSequencer(sequencer);
		initParser();
	}

	/**
	 * Creates a new Player instance using a Sequencer obtained from the
	 * Synthesizer that you have provided.
	 * 
	 * @param synth
	 *            The Synthesizer you want to use for this Player.
	 */
	public Player(Synthesizer synth) throws MidiUnavailableException {
		this(Player.getSequencerConnectedToSynthesizer(synth));
	}

	private void initParser() {
		this.parser = new MusicStringParser();
		this.renderer = new MidiRenderer(sequenceTiming, resolution);
		this.parser.addParserListener(this.renderer);
	}

	private void initSequencer() {
		// Close the sequencer and synthesizer
		getSequencer().addMetaEventListener(event -> {
			if (event.getType() == 47) {
				close();
			}
		});
	}

	private void openSequencer() {
		if (getSequencer() == null) {
			throw new JFugueException(
					JFugueException.SEQUENCER_DEVICE_NOT_SUPPORTED);
		}

		// Open the sequencer, if it is not already open
		if (!getSequencer().isOpen()) {
			try {
				getSequencer().open();
			} catch (MidiUnavailableException e) {
				throw new JFugueException(
						JFugueException.SEQUENCER_DEVICE_NOT_SUPPORTED_WITH_EXCEPTION
								+ e.getMessage());
			}
		}
	}

	/**
	 * Plays a pattern by setting up a Renderer and feeding the pattern to it.
	 * 
	 * @param pattern
	 *            the pattern to play
	 * @see MidiRenderer
	 */
	public void play(Pattern pattern) {
		Sequence sequence = getSequence(pattern);
		play(sequence);
	}

	/**
	 * Plays a pattern by setting up a Renderer and feeding the pattern to it.
	 * 
	 * @param rhythm
	 *            the pattern to play
	 * @see MidiRenderer
	 */
	public void play(Rhythm rhythm) {
		Pattern pattern = rhythm.getPattern();
		Sequence sequence = getSequence(pattern);
		play(sequence);
	}

	/**
	 * Plays a MIDI Sequence
	 * 
	 * @param sequence
	 *            the Sequence to play
	 * @throws JFugueException
	 *             if there is a problem playing the music
	 * @see MidiRenderer
	 */
	private void play(Sequence sequence) {
		// Open the sequencer
		openSequencer();

		// Set the sequence
		try {
			getSequencer().setSequence(sequence);
		} catch (Exception e) {
			throw new JFugueException(
					JFugueException.ERROR_PLAYING_MUSIC + e.getMessage());
		}

		setStarted(true);

		// Start the sequence
		getSequencer().start();

		// Wait for the sequence to finish
		while (isPlaying() || isPaused()) {
			try {
				Thread.sleep(20); // don't hog all of the CPU
			} catch (InterruptedException e) {
				throw new JFugueException(JFugueException.ERROR_SLEEP);
			}
		}

		// Close the sequencer
		getSequencer().close();

		setStarted(false);
		setFinished(true);
	}

	/**
	 * Plays a string of music. Be sure to call player.close() after play() has
	 * returned.
	 * 
	 * @param musicString
	 *            the MusicString (JFugue-formatted string) to play
	 * @version 3.0
	 */
	public void play(String musicString) {
		if (musicString.indexOf(".mid") > 0) {
			// If the user tried to call this method with "filename.mid" or
			// "filename.midi", throw the following exception
			throw new JFugueException(
					JFugueException.PLAYS_STRING_NOT_FILE_EXC);
		}

		Pattern pattern = new Pattern(musicString);
		play(pattern);
	}

	/**
	 * Plays a MIDI file, without doing any conversions to MusicStrings. Be sure
	 * to call player.close() after play() has returned.
	 * 
	 * @param file
	 *            the MIDI file to play
	 * @throws IOException
	 * @throws InvalidMidiDataException
	 * @version 3.0
	 */
	public void playMidiDirectly(File file)
			throws IOException, InvalidMidiDataException {
		Sequence sequence = MidiSystem.getSequence(file);
		play(sequence);
	}

	/**
	 * Plays a URL that contains a MIDI sequence. Be sure to call player.close()
	 * after play() has returned.
	 * 
	 * @param url
	 *            the URL to play
	 * @throws IOException
	 * @throws InvalidMidiDataException
	 * @version 3.0
	 */
	public void playMidiDirectly(URL url)
			throws IOException, InvalidMidiDataException {
		Sequence sequence = MidiSystem.getSequence(url);
		play(sequence);
	}

	public void play(Anticipator anticipator, Pattern pattern, long offset) {
		Sequence sequence = getSequence(pattern);
		Sequence sequence2 = getSequence(pattern);
		play(anticipator, sequence, sequence2, offset);
	}

	public void play(Anticipator anticipator, Sequence sequence,
			Sequence sequence2, long offset) {
		anticipator.play(sequence);

		if (offset > 0) {
			try {
				Thread.sleep(offset);
			} catch (InterruptedException e) {
				throw new JFugueException(JFugueException.ERROR_SLEEP);
			}
		}

		play(sequence2);
	}

	/**
	 * Closes MIDI resources - be sure to call this after play() has returned.
	 */
	public void close() {
		getSequencer().close();
		try {
			if (MidiSystem.getSynthesizer() != null) {
				MidiSystem.getSynthesizer().close();
			}
		} catch (MidiUnavailableException e) {
			throw new JFugueException(
					JFugueException.GENERAL_ERROR + e.getMessage());
		}
	}

	private void setStarted(boolean started) {
		this.started = started;
	}

	private void setFinished(boolean finished) {
		this.finished = finished;
	}

	public boolean isStarted() {
		return this.started;
	}

	public boolean isFinished() {
		return this.finished;
	}

	public boolean isPlaying() {
		return getSequencer().isRunning();
	}

	public boolean isPaused() {
		return paused;
	}

	public void pause() {
		paused = true;
		if (isPlaying()) {
			getSequencer().stop();
		}
	}

	public void resume() {
		paused = false;
		getSequencer().start();
	}

	public void stop() {
		paused = false;
		getSequencer().stop();
		getSequencer().setMicrosecondPosition(0);
	}

	public void jumpTo(long microseconds) {
		getSequencer().setMicrosecondPosition(microseconds);
	}

	public long getSequenceLength(Sequence sequence) {
		return sequence.getMicrosecondLength();
	}

	public long getSequencePosition() {
		return getSequencer().getMicrosecondPosition();
	}

	/**
	 * Saves the MIDI data from a pattern into a file.
	 * 
	 * @param pattern
	 *            the pattern to save
	 * @param file
	 *            the File to save the pattern to. Should include file
	 *            extension, such as .mid
	 */
	public void saveMidi(Pattern pattern, File file) throws IOException {
		Sequence sequence = getSequence(pattern);

		int[] writers = MidiSystem.getMidiFileTypes(sequence);
		if (writers.length == 0) {
			return;
		}

		MidiSystem.write(sequence, writers[0], file);
	}

	/**
	 * Saves the MIDI data from a MusicString into a file.
	 * 
	 * @param musicString
	 *            the MusicString to save
	 * @param file
	 *            the File to save the MusicString to. Should include file
	 *            extension, such as .mid
	 */
	public void saveMidi(String musicString, File file) throws IOException {
		Pattern pattern = new Pattern(musicString);
		saveMidi(pattern, file);
	}

	/**
	 * Parses a MIDI file and returns a Pattern. This is an excellent example of
	 * JFugue's Parser-Renderer architecture:
	 *
	 * <pre>
	 * MidiParser parser = new MidiParser();
	 * MusicStringRenderer renderer = new MusicStringRenderer();
	 * parser.addParserListener(renderer);
	 * parser.parse(sequence);
	 * </pre>
	 *
	 * @param file
	 *            The MIDI file
	 * @return a Pattern containing the MusicString representing the MIDI music
	 * @throws IOException
	 *             If there is a problem opening the MIDI file
	 * @throws InvalidMidiDataException
	 *             If there is a problem obtaining MIDI resources
	 */
	public Pattern loadMidi(File file)
			throws IOException, InvalidMidiDataException {
		MidiFileFormat format = MidiSystem.getMidiFileFormat(file);
		this.sequenceTiming = format.getDivisionType();
		this.resolution = format.getResolution();
		return Pattern.loadMidi(file);
	}

	public static void allNotesOff() {
		try {
			allNotesOff(MidiSystem.getSynthesizer());
		} catch (MidiUnavailableException e) {
			throw new JFugueException(JFugueException.GENERAL_ERROR);
		}
	}

	/**
	 * Stops all notes from playing on all MIDI channels.
	 */
	public static void allNotesOff(Synthesizer synth) {
		try {
			if (!synth.isOpen()) {
				synth.open();
			}
			MidiChannel[] channels = synth.getChannels();
			for (int i = 0; i < channels.length; i++) {
				channels[i].allNotesOff();
			}
		} catch (MidiUnavailableException e) {
			throw new JFugueException(JFugueException.GENERAL_ERROR);
		}
	}

	/**
	 * Returns the sequencer containing the MIDI data from a pattern that has
	 * been parsed.
	 * 
	 * @return the Sequencer from the pattern that was recently parsed
	 */
	public Sequencer getSequencer() {
		return this.sequencer;
	}

	private void setSequencer(Sequencer sequencer) {
		this.sequencer = sequencer;
		initSequencer();
	}

	/**
	 * Returns the sequence containing the MIDI data from the given pattern.
	 * 
	 * @return the Sequence from the given pattern
	 */
	public Sequence getSequence(Pattern pattern) {
		this.renderer.reset();
		this.parser.parse(pattern);
		Sequence sequence = this.renderer.getSequence();
		return sequence;
	}

	/**
	 * Returns an instance of a Sequencer that uses the provided Synthesizer as
	 * its receiver. This is useful when you have made changes to a specific
	 * Synthesizer--for example, you've loaded in new patches--that you want the
	 * Sequencer to use. You can then pass the Sequencer to the Player
	 * constructor.
	 *
	 * @param synth
	 *            The Synthesizer to use as the receiver for the returned
	 *            Sequencer
	 * @return a Sequencer with the provided Synthesizer as its receiver
	 * @throws MidiUnavailableException
	 * @version 4.0
	 */
	public static Sequencer getSequencerConnectedToSynthesizer(
			Synthesizer synth) throws MidiUnavailableException {
		Sequencer sequencer = MidiSystem.getSequencer(false); // Get Sequencer
																// which is not
																// connected to
																// new
																// Synthesizer.
		sequencer.open();
		if (!synth.isOpen()) {
			synth.open();
		}
		sequencer.getTransmitter().setReceiver(synth.getReceiver()); // Connect
																		// the
																		// Synthesizer
																		// to
																		// our
																		// synthesizer
																		// instance.
		return sequencer;
	}
}